// Written in the D Programming Language
/**
 * The 24th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=24">NeHe tutorial series</a>.
 * Originally written by Jeff Molofee.
 *
 * Authors: Jeff Molofee
 *          Olli Aalto
 */
module lesson24;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import tango.text.Util;
import Int = tango.text.convert.Integer;

/// The window title
const char[] WINDOW_TITLE = "NeHe's Token, Extensions, Scissoring & TGA Loading Tutorial (D version)";

/// The main loop flag
bool running;
/// Used For Scrolling The Screen
int scroll;
/// Keeps Track Of The Number Of Extensions Supported
int maxtokens;
/// Scissor Width
int swidth;
/// Scissor Height
int sheight;
/// Used to control the Scrolling action
int scroller;
/// All the available extensions
char[][] extensions;
/// Your display cards name 
char[] renderer;
/// Your display cards vendor
char[] vendor;
/// The version of the driver used
char[] driverVersion;

const RENDERER_TEXT = "Renderer";
const VENDOR_TEXT = "Vendor";
const VERSION_TEXT = "Version";
const NEHE_PRODUCTIONS_TEXT = "NeHe Productions";

/// Storage for font texture
GLuint texture;
/// Base Display List For The Font
GLuint base;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	// Clean up our font list
	glDeleteLists(base, 256);

	// Clean up our texture
	glDeleteTextures(1, &texture);

	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if(args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	// Get all the available extensions
	char[] extensionString = fromStringz(glGetString(GL_EXTENSIONS));
	// Split them, this replaces the token stuff in the c/c++ version
	extensions = split!(char)(extensionString, " ");

	// Get the name of your display card
	renderer = fromStringz(glGetString(GL_RENDERER));
	// Get the name of its vendor
	vendor = fromStringz(glGetString(GL_VENDOR));
	// Get the version of the driver used
	driverVersion = fromStringz(glGetString(GL_VERSION));

	running = true;
	while(running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);

		// Update scrolling if either the down key or up key is being pressed
		if(scroller == -1)
		{
			if(scroll > 0)
			{
				scroll -= 2;
			}
		}
		else if(scroller == 1)
		{
			if(scroll < 32 * (maxtokens - 9))
			{
				scroll += 2;
			}
		}
	}
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while(SDL_PollEvent(&event))
	{
		switch(event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch(key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_UP:
		case SDLK_DOWN:
			// No scrolling
			scroller = 0;
			break;
		default:
			break;
	}
}

/**
 * Process a key pressed event.
 */
void keyPressed(int key)
{
	switch(key)
	{
		case SDLK_UP:
			// Scroll Up
			scroller = -1;
			break;
		case SDLK_DOWN:
			// Scroll Down
			scroller = 1;
			break;
		default:
			break;
	}
}

/**
 * Load the texture used in this tutorial.
 */
void loadGLTextures()
{
	SDL_Surface* fontTexture;

	// Load The Bitmap Font, Check For Errors, If the Bitmap's Not Found Quit
	if((fontTexture = SDL_LoadBMP("data/Font.bmp")) !is null)
	{
		// Free the surface when exiting the scope
		scope(exit)
			SDL_FreeSurface(fontTexture);

		// Create The Texture
		glGenTextures(1, &texture);

		// Create Nearest Filtered Texture
		glBindTexture(GL_TEXTURE_2D, texture);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, fontTexture.w, fontTexture.h, 0, GL_BGR, GL_UNSIGNED_BYTE, fontTexture.pixels);
	}
}

/**
 * Resize and initialize the OpenGL window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	// Set Scissor Width To Window Width
	swidth = width;
	// Set Scissor Height To Window Height
	sheight = height;
	// Reset The Current Viewport
	glViewport(0, 0, width, height);
	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();
	// Create Ortho widthxheight View (0,0 At Top Left)
	glOrtho(0.0f, width, height, 0.0f, -1.0f, 1.0f);
	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * Function to build our font list. 
 */
void buildFont()
{
	// Creating 256 Display List
	base = glGenLists(256);
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, texture);

	// Loop Through All 256 Lists
	for(int i = 0; i < 256; i++)
	{
		/* NOTE:
		 *  BMPs are stored with the top-leftmost pixel being the
		 * last byte and the bottom-rightmost pixel being the first
		 * byte. So an image that is displayed as
		 *    1 0
		 *    0 0
		 * is represented data-wise like
		 *    0 0
		 *    0 1
		 * And because SDL_LoadBMP loads the raw data without
		 * translating to how it is thought of when viewed we need
		 * to start at the bottom-right corner of the data and work
		 * backwards to get everything properly. So the below code has
		 * been modified to reflect this. Examine how this is done and
		 * how the original tutorial is done to grasp the differences.
		 *
		 * As a side note BMPs are also stored as BGR instead of RGB
		 * and that is why we load the texture using GL_BGR. It's
		 * bass-ackwards I know but whattaya gonna do?
		 */

		// X Position Of Current Character
		float cx = 1 - cast(float) (i % 16) / 16.0f;
		// Y Position Of Current Character
		float cy = 1 - cast(float) (i / 16) / 16.0f;

		// Start Building A List
		glNewList(base + (255 - i), GL_COMPILE);
		{
			// Use A Quad For Each Character
			glBegin(GL_QUADS);
			{
				// Texture Coord (Bottom Left)
				glTexCoord2f(cx - 0.0625, cy);
				// Vertex Coord (Bottom Left)
				glVertex2i(0, 16);

				// Texture Coord (Bottom Right)
				glTexCoord2f(cx, cy);
				// Vertex Coord (Bottom Right)
				glVertex2i(16, 16);

				// Texture Coord (Top Right)
				glTexCoord2f(cx, cy - 0.0625f);
				// Vertex Coord (Top Right)
				glVertex2i(16, 0);

				// Texture Coord (Top Left)
				glTexCoord2f(cx - 0.0625f, cy - 0.0625f);
				// Vertex Coord (Top Left)
				glVertex2i(0, 0);
			}
			glEnd();

			// Move To The Left Of The Character
			glTranslated(15, 0, 0);
		}
		glEndList();
	}
}

/**
 * Initialize OpenGL.
 */
void initGL()
{
	// Jump To Texture Loading Routine
	loadGLTextures();

	// Build The Font
	buildFont();

	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
}

/**
 * This Is Where The Printing Happens.
 * 
 * Params:
 *     x = the x coordinate of the text
 *     y = the y coordinate of the text
 *     set = font set
 *     string = the text to be printed
 */
void glPrint(GLint x, GLint y, int set, char[] string)
{
	if(set > 1)
	{
		set = 1;
	}
	// Enable textures
	glEnable(GL_TEXTURE_2D);
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, texture);
	// Disables Depth Testing
	glDisable(GL_DEPTH_TEST);
	// Reset The Modelview Matrix
	glLoadIdentity();
	// Position The Text (0,0 - Bottom Left)
	glTranslated(x, y, 0);
	// Choose The Font Set (0 or 1)
	glListBase(base - 32 + (128 * set));
	// If Set 0 Is Being Used Enlarge Font
	if(set == 0)
	{
		// Enlarge Font Width And Height
		glScalef(1.5f, 2.0f, 1.0f);
	}
	// Write The Text To The Screen
	glCallLists(string.length, GL_UNSIGNED_BYTE, toStringz(string));
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// Disable textures
	glDisable(GL_TEXTURE_2D);
}

/**
 * The drawing function. Now we only clear the color and depht buffers, so that
 * the window stays black.
 */
void drawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer

	glColor3f(1.0f, 0.5f, 0.5f); // Set Color To Bright Red
	glPrint(50, 16, 1, RENDERER_TEXT); // Display Renderer
	glPrint(80, 48, 1, VENDOR_TEXT); // Display Vendor Name
	glPrint(66, 80, 1, VERSION_TEXT); // Display Version

	glColor3f(1.0f, 0.7f, 0.4f); // Set Color To Orange
	glPrint(200, 16, 1, renderer); // Display Renderer
	glPrint(200, 48, 1, vendor); // Display Vendor Name
	glPrint(200, 80, 1, driverVersion); // Display Version

	glColor3f(0.5f, 0.5f, 1.0f); // Set Color To Bright Blue
	glPrint(192, 432, 1, NEHE_PRODUCTIONS_TEXT); // Write NeHe Productions At The Bottom Of The Screen

	glLoadIdentity(); // Reset The ModelView Matrix
	glColor3f(1.0f, 1.0f, 1.0f); // Set The Color To White
	glBegin(GL_LINE_STRIP); // Start Drawing Line Strips (Something New)
	glVertex2d(639, 417); // Top Right Of Bottom Box
	glVertex2d(0, 417); // Top Left Of Bottom Box
	glVertex2d(0, 480); // Lower Left Of Bottom Box
	glVertex2d(639, 480); // Lower Right Of Bottom Box
	glVertex2d(639, 128); // Up To Bottom Right Of Top Box
	glEnd(); // Done First Line Strip
	glBegin(GL_LINE_STRIP); // Start Drawing Another Line Strip
	glVertex2d(0, 128); // Bottom Left Of Top Box
	glVertex2d(639, 128); // Bottom Right Of Top Box								
	glVertex2d(639, 1); // Top Right Of Top Box
	glVertex2d(0, 1); // Top Left Of Top Box
	glVertex2d(0, 117); // Down To Top Left Of Bottom Box
	glEnd(); // Done Second Line Strip

	glScissor(1, cast(int) (0.135416f * sheight), swidth - 4, cast(int) (0.597916f * sheight)); // Define Scissor Region
	glEnable(GL_SCISSOR_TEST); // Enable Scissor Testing

	// Loop through all the extensions
	foreach(cnt, extension; extensions)
	{
		// Is 'maxtokens' Less Than 'cnt'
		if(maxtokens < cnt)
		{
			maxtokens = cnt; // If So, Set 'maxtokens' Equal To 'cnt'
		}

		glColor3f(0.5f, 1.0f, 0.5f); // Set Color To Bright Green
		glPrint(0, 96 + (cnt * 32) - scroll, 0, Int.toString(cnt)); // Print Current Extension Number
		glColor3f(1.0f, 1.0f, 0.5f); // Set Color To Yellow
		glPrint(75, 96 + (cnt * 32) - scroll, 0, extension); // Print The Current Token (Parsed Extension Name)
	}

	glDisable(GL_SCISSOR_TEST); // Disable Scissor Testing
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits, bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	// Note the SDL_DOUBLEBUF flag is not required to enable double
	// buffering when setting an OpenGL video mode.
	// Double buffering is enabled or disabled using the
	// SDL_GL_DOUBLEBUFFER attribute. (See above.)
	int mode = SDL_OPENGL;
	if(fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if(SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
